<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>页面加载海量数据</title>
    <style>
      ul,
      li {
        padding: 0;
        margin: 0;
        list-style: none;
      }
      li {
        text-align: center;
        line-height: 30px;
        float: left;
        width: 100%;
        /* height: 30px; */
        border-bottom: 1px solid #ccc;
      }
      .totalBox {
        width: 100%;
        /* background-color: pink; */
      }
      .listBox {
        border: 1px solid blue;
        height: 600px;
        box-sizing: content-box;
        width: 100%;
      }
    </style>
  </head>

  <body>
    <button id="button">开始加载数据</button><br />
    <div
      id="container"
      style="
        margin: auto;

        height: 600px;
        border: 1px solid #ccc;
        overflow-y: scroll;
      "
    ></div>

    <script>
      // document.getElementById('button').addEventListener('click', function () {
      //   // 记录任务开始时间
      //   let now = Date.now()
      //   // 插入一万条数据
      //   const total = 10000
      //   // 获取容器
      //   let ul = document.getElementById('container')
      //   // 将数据插入容器中
      //   for (let i = 0; i < total; i++) {
      //     let li = document.createElement('li')
      //     li.innerText = 'gagagaga'
      //     ul.appendChild(li)
      //   }
      //   console.log('JS运行时间：', Date.now() - now)
      //   setTimeout(() => {
      //     console.log('总运行时间：', Date.now() - now)
      //   }, 0)

      //   // print JS运行时间： 38
      //   // print 总运行时间： 957
      //   // 在 JS 的Event Loop中，当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后，才会触发渲染线程对页面进行渲染
      //   // 第一个console.log的触发时间是在页面进行渲染之前，此时得到的间隔时间为JS运行所需要的时间
      //   // 第二个console.log是放到 setTimeout 中的，它的触发时间是在渲染完成，在下一次Event Loop中执行的

      //   // 浏览器从Performance可以看出，代码从执行到渲染结束，共消耗了960.8ms
      // })

      // 虚拟滚动 （面向对象）
      // 思路：一共需要画出来三个盒子模型,最外层的作为页面固定视图盒子,第二个盒子是数据总数量撑开的盒子,做隐藏处理,第三个可以是滚动滑块的盒子,用在第二个真是盒子上面,根据
      // 鼠标滚动距离的改变而改变,每次滚动触发视图更新,原理就是计算出滚动距离改变了多少下标再根据下标截取当前滚动到的数据 创建虚拟标签,更新dom.
      // ps:性能优化问题是指在dom渲染上面,消耗大量性能,和数据本身数量大小关系并不大,只要解决了dom渲染优化,性能自然优化.这里就是根据每次滚动后一次性更新一个dom,而不是每个数据更新遍历一次dom节点.

      class DevDom {
        constructor(op) {
          this.el = op.el
          this.liHeight = op.liHeight
          this.data = op.data
          this.list = []
          this.offsetY = 0
          this.tolHeight = this.data.length * this.liHeight //所有数据总高度
          this.lastTime = new Date().getTime()
        }
        init() {
          this.create()
          this.initData()
          // 初始化滚动事件
          this.el.addEventListener(
            'scroll',
            //  这里本来应该写滚动函数,又去this指向发生改变,所以直接借用滚动函数的this过来
            this.handeScroll.bind(this), //ps 这里需要用bind函数不能用call和apply,详情可参考(this指向问题2文章)
            false
          )
        }
        create() {
          // 生成dom结构
          let totalBox = document.createElement('div') // 生成总盒子 超出隐藏
          totalBox.className = 'totalBox'
          totalBox.style.height = this.tolHeight + 'px'

          let listBox = document.createElement('div') // 生成可视区域盒子
          listBox.className = 'listBox'
          totalBox.append(listBox)
          this.el.append(totalBox)
          this.totalBox = totalBox
          this.listBox = listBox
        }
        initData() {
          // 计算行数 由于固定高度为600px
          this.showNum = 600 / this.liHeight
          // this.showNum = Math.floor(600 / this.liHeight) + 4
          this.list = this.data.slice(0, this.showNum)
          this.createByList(this.list)
        }
        createByList(list) {
          this.listBox.innerText = '' // 不能缺少 每次重新渲染需要清空上次记录
          let fragment = document.createDocumentFragment()
          // 生成虚拟标签每次循环都添加在虚拟标签里面 减少dom操作
          for (let i = 0; i < this.showNum; i++) {
            let li = document.createElement('li')
            li.style.height = this.liHeight + 'px'
            li.innerText = list[i]
            fragment.append(li)
          }
          this.listBox.append(fragment)
        }
        handeScroll(e) {
          // 优化处理 滚动事件给一个延迟处理 防止重复滚动

          if (new Date().getTime() - this.lastTime > 20) {
            // 滚动事件(重点)
            let scrollTop = e.target.scrollTop //滚动距离
            let prevNum = Math.floor(scrollTop / this.liHeight) //向下取整 (展示滚动的行数)
            this.offsetY = scrollTop - (scrollTop % this.liHeight) //需要偏移的距离
            // console.log(this.offsetY, ' this.offsetY')
            // console.log(this.data, 'this.data')
            // 获取新滚动的数据进行加载渲染
            // 根据下标的变化 由于这次滚动距离scrollTop,可以得到prevNum为改变的位置 也就是下标,再和上一次滚动的位置得到一个新的结束位置下标,可以得到新展示区域的数据
            this.list = this.data.slice(prevNum, prevNum + this.showNum)
            // console.log(this.list, '    this.list')
            // 新的到的数据再次构建虚拟节点
            this.createByList(this.list)
            this.listBox.style.transform = `translateY(${this.offsetY}px)` //可视区域随之改变y距离
            this.lastTime = new Date().getTime()
          }
        }
      }

      document.getElementById('button').addEventListener('click', function () {
        // 模拟数据生成
        let p = new Promise((resolve, reject) => {
          let t = 100000 // 需要加载的海量数据
          let data = []
          for (let i = 0; i < t; i++) {
            data.push(`我是生成的${i + 1}条数据`)
          }
          resolve(data)
        })
        p.then((res) => {
          new DevDom({
            el: document.getElementById('container'),
            liHeight: 30, //每一行行数据高度，
            data: res, //数据源
          }).init()
        })
      })
    </script>
  </body>
</html>
